객체지향 설계 5대 원칙 - SOLID 원칙

들어가기

SOLID(SRP, OCP, LSP, ISP, DIP)의 개념을 정리하고 이해해보겠습니다.

본론

단일 책임 원칙

SOLID의 첫 번째 원칙으로 단일 책임 원칙(Single Responsibiliy Principle)이 있습니다. 이는 말 그대로 객체는 단 하나의 책임만 가져야 한다는 의미입니다.

그렇다면 여기서 의미하는 책임이란 무엇일까요? 이는 객체가 해야하는 것 또는 가장 대표되는 기능으로 객체 자신만이 수행할 수 있는 기능을 의미합니다.

public class Student {
    // 수강 과목 보기
    public void getCourses() {...}
    // 새로운 과목 등록
    public void setCourses() {...}
    // 데이터 베이스에 성적 저장
    public void saveGrade() {...}
    // 데이터베이스에서 성적 조회
    public void loadGrade() {...}
    // 성적표 출력
    public void printOnReportCard() {...}
    // 출석부 출력
    public void printOnAttendanceBook() {...}
}

위의 예시를 한번 확인해봅시다. Student 클래스는 너무나 많은 책임을 수행해야합니다. 데이터베이스에 성적을 저장하거나 확인 또는 성적표와 출석부를 출력하는 일은 Student 클래스가 아닌 다른 클래스가 더 잘할 수 있는 여지가 있습니다.

즉, Student가 가장 잘 할수 있는 책임은 수강 과목을 등록하고 조회하는 일입니다.

예시처럼 클래스가 작성되었다고 한다면 예측하지 못한 변경 사항이 발생했을때 유연하고 확장성있게 시스템을 변경할 수가 없습니다. 한번 상상을 해보겠습니다.

  • 성적표와 출석부 이외의 다른 형식으로 학생정보를 출력하는 경우.
  • 성적 스키마가 변경되었을 경우.

이러한 사항은 모두 실질적으로 학생 클래스의 핵심기능과는 상관없지만 클래스를 수정해야하는 이유가 됩니다. 또한 책임이 많아 질수록 코드끼리의 결합성이 올라갑니다. 예를 들어 수강 과목을 조회하는 코드와 데이터베이스에서 학생 정보를 가져오는 코드 중 어딘가가 연결될 수도 있고, 수강 과목을 추가하는 코드와 데이터베이스에 학생 정보를 갱신하는 코드가 연결될 수 있습니다.

따라서 이런 여러 책임을 수행하는 Student 클래스는단 하나의 책임만 수행하도록 변경해야합니다.

다음과 같이 Student 클래스는 학생 고유의 역할을 수행하고 나머지 변경이 잦은 책임들은 따로 클래스로 분리하여 데이터베이스의 스키마가 변경되더라도 Student 클래스에는 영향을 끼치지 않도록 해야합니다.

개방 폐쇄 원칙

개방 폐쇄 원칙(Open Closed Principle)은 기능의 추가에는 열려 있으면서 수정에는 닫혀있어야한다는 뜻입니다. 즉, 기존의 코드 변경을 최소화하면서 새로운 기능을 추가하고 수정한다는 의미입니다.

그림처럼 성적표와 출석부를 출력하는 클래스가 설계되어있다고 가정해봅시다. 만약 여기서 Client를 이용해 도서대출 현황을 출력하고 싶다면 어떻게 설계하시겠습니까?

따로 도서대출 클래스를 만들어 Client와 연결하면 될것이라 생각됩니다. 하지만 이는 Client의 코드를 수정할 가능성이 있어 OCP 원칙을 위반합니다.

즉, 애초에 Client 클래스는 수정을 하지 않고 기능을 추가할 수 있게 설계해야했습니다. 과연 어떻게 설계를 해야했을까요?

기존 설계를 위의 그림처럼 진행했다면 조금 더 변화에 유연하게 대처 가능했을 것입니다. Client는 어떤 새로운 출력물이 추가되어도 변경되지 않습니다.

리스코프 치환 원칙

리스코프 치환 원칙(Liskov Substitution Principle)을 만족하면 부모 클래스의 인스턴스 대신에 자식 클래스의 인스턴스로 대체해도 프로그램이 정상적으로 동작해야함을 의미합니다.

위와 같은 예시는 LSP가 성립된다고 볼수 있습니다. 원숭이 인스턴스는 포유류와 원숭이 둘다 성립됨으로 리스코프 치환 원칙(Liskov Substitution Principle)이 성립됩니다.

이번에는 코드로 한번 확인해보겠습니다.

public class Bag {
    private int price;
    public void setPrice(int price) { this.price = price; };
    public void getPrice() { return price; };
}

public class DiscountedBag extends Bag {
    private double discountRate;
    public void setDiscount(double discountRate) { this.discountRate = discountRate; };
    public void applyDiscount(int price) { super.setPrice(price - (int)(discountRate * price)) };
}

위의 코드는 모두 같은 값을 출력합니다. 또, b3과 b4 인스턴스를 자식 객체의 인스턴스로 바꾸어도 정상 작동이 됩니다. 이는 상속관계가 LSP를 위반하지 않았음을 의미합니다.

하지만 위의 코드에서 DiscountedBag 클래스에서 Bag 클래스의 메서드를 재정의하는 순간 LSP를 만족하지 않습니다. 즉, LSP를 만족시키는 간단한 방법은 재정의를 하지 않는것 입니다.

의존 역전 원칙

의존 역전 원칙(Dependency Inversion Principle)은 객체 사이에 도움을 주고 받으며 발생하는 의존 관계에 있어 일종의 가이드 라인을 제시합니다. DIP는 의존 관계를 맺을 때 변화하기 쉬운 것 또는 자주 변하는 것보다 변화가 없는 것에 의존하라는 원칙입니다.

변하기 어려운 것??? 즉, 추상 클래스 또는 인터페이스에 의존하라는 의미입니다.

변하기 쉬운 것에 의존한다면 해당 클래스가 변화할때마다 그 클래스에 의존하는 다른 클래스 또한 수정해야하는 일이 일어납니다. 이에따라 변하기 쉬운 클래스를 추상화하는 인터페이스에 의존하는것이 DIP가 성립하는 설계입니다.

인터페이스 분리 원칙

인터페이스 분리 원칙(Interface Segregation Principle)은 클라이언트 입장에서 자신이 이용하지 않는 기능에는 영향을 받지 않아야한다는 의미입니다.

그림과 같은 설계는 프린터만 요구하는 클라이언트가 복합기의 팩스 기능의 변경으로 인해 발생하는 문제에 영향을 받을 수 있습니다.

즉, ISP를 다르게 설명하면 말 그대로 인터페이스를 클라이언트에 특화되도록 분리시키는 설계입니다.

그림처럼 코드를 작성하면 클라이언트 객체들마다 관심을 갖는 메서드만 있는 인터페이스만을 제공받습니다. 이렇게 설계하면 클라이언트 입장에서는 쓰지 않는 메서드에 생긴 변화로 인한 영향을 받지 않습니다

마치며

SOLID의 개념과 관련해 추가적인 질문이나 오류, 오타가 있을시 댓글로 남겨주세요.

출처

JAVA 객체지향 디자인패턴

Share